package edu.chl.jadetalk.business.upload;

import java.io.*;
import java.math.BigInteger;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.*;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPReply;

/**
 * Handles storing and retrieving of images from/to the remote FTP server.
 * Maintains a list of files preventing identical files being saved twice.
 * 
 * TODO: Singleton or not? Multiple FTP connections for all or one per user? (file list would have to be managed elsewhere then)
 * 
 * @author Erik
 */
@Singleton
@TransactionManagement(value = TransactionManagementType.CONTAINER)
@TransactionAttribute(value = TransactionAttributeType.REQUIRED)
public class ImageDataManagerBean implements ImageDataManagerBeanLocal {

    private String FTP_HOST = "danik.gotdns.org";
    private int FTP_PORT = 21;
    private String FTP_USERNAME = "jadetalk";
    private String FTP_PASSWORD = "jadetalk";
    private String FTP_IMAGE_DIR = "jadetalk";
    @EJB
    private ImageCacheLocal imageCache;
    private FTPClient ftp;
    private Collection<String> fileList;

    @Override
    public byte[] getImage(String filename) throws NoSuchImageException {

        Logger.getAnonymousLogger().log(Level.INFO, String.format("getImage('%s')", filename));

        // Look for image in cache
        Byte[] imageBytes = imageCache.getImage(filename);
        if (imageBytes != null) {
            // Image found in cache, convert to byte[] and return.
            byte[] result = new byte[imageBytes.length];
            for (int i = 0; i < imageBytes.length; i++) {
                result[i] = imageBytes[i];
            }
            return result;
        }

        // Retrieve image from FTP server
        try {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Retrieving image '%s' from FTP server (%s:%d)", filename, FTP_HOST, FTP_PORT));

            ByteArrayOutputStream output = new ByteArrayOutputStream();

            if (ftp.retrieveFile(filename, output)) {

                byte[] result = output.toByteArray();

                Logger.getAnonymousLogger().log(
                        Level.INFO,
                        String.format("'%s' (%d bytes) retrieved from FTP server (%s:%d).",
                        filename,
                        result.length,
                        FTP_HOST,
                        FTP_PORT));

                // Save image to cache
                saveImageToCache(filename, result);

                return result;
            } else {
                throw new NoSuchImageException(String.format("Failed to retrieve image '%s' from FTP server (%s:%d)", filename, FTP_HOST, FTP_PORT));
            }
        } catch (Exception ex) {
            throw new NoSuchImageException(String.format("Failed to retrieve image '%s' from FTP server (%s:%d)", filename, FTP_HOST, FTP_PORT), ex);
        }
    }

    @Override
    public String getFileName(byte[] image) {
        try {
            MessageDigest digester = MessageDigest.getInstance("SHA1");

            // SHA-1 output is 160 bits, shorten it by using the absolute value and truncating it to 32 bits
            // TODO: Will collisions be a problem?
            BigInteger truncated = (new BigInteger(digester.digest(image)).and(BigInteger.valueOf(0xffffffffl))).abs();
            return truncated.toString(16);

        } catch (NoSuchAlgorithmException ex) {
            throw new EJBException(ex);
        }
    }

    @Override
    public void saveImageToCache(String filename, byte[] image) {
        if (image.length > 0 && imageCache.getImage(filename) == null) {
            imageCache.putImage(filename, image);
        }
    }

    @Override
    @Asynchronous
    public void uploadImage(String filename, byte[] image) {
        // Only save image if it does not already exist
        if (!fileList.contains(filename)) {

            Logger.getAnonymousLogger().log(Level.INFO, String.format("Image '%s' not found in cache, uploading to FTP server (%s:%d).", filename, FTP_HOST, FTP_PORT));

            ByteArrayInputStream stream = null;

            try {
                stream = new ByteArrayInputStream(image);

                if (ftp.storeFile(filename, stream)) {
                    Logger.getAnonymousLogger().log(Level.INFO, String.format("Image '%s' successfully uploaded to FTP server.", filename));
                } else {
                    Logger.getAnonymousLogger().log(Level.SEVERE, String.format("Failed to upload '%s' to FTP server (%s:%d).", filename, FTP_HOST, FTP_PORT));
                }
            } catch (Exception ex) {
                throw new EJBException(ex);
            } finally {
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException ex) {
                        Logger.getAnonymousLogger().log(Level.WARNING, "Failed to close stream.");
                    }
                }
            }
        }
    }

    @PostConstruct
    private void connectFTP() throws SocketException, IOException {
        // Establish connection with FTP server
        ftp = new FTPClient();
        ftp.setControlKeepAliveTimeout(100);
        ftp.connect(FTP_HOST, FTP_PORT);

        // Verify connection successful
        if (!FTPReply.isPositiveCompletion(ftp.getReplyCode())) {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("FTP server '%s:%d' refused connection.", FTP_HOST, FTP_PORT));
            ftp.disconnect();
            throw new EJBException(String.format("FTP server '%s:%d' refused connection.", FTP_HOST, FTP_PORT));
        } else {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Sucessfully connected to FTP server '%s:%d'.", FTP_HOST, FTP_PORT));
        }

        // Verify login successful
        if (!ftp.login(FTP_USERNAME, FTP_PASSWORD)) {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("FTP server '%s:%d' refused login (%s//%s).", FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD));
            ftp.disconnect();
            throw new EJBException(String.format("FTP server '%s:%d' refused login (%s//%s).", FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD));
        } else {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Sucessfully logged in as %s//%s at FTP server '%s:%d'.", FTP_USERNAME, FTP_PASSWORD, FTP_HOST, FTP_PORT));
        }

        // Set working directory
        if (ftp.changeWorkingDirectory(FTP_IMAGE_DIR)) {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Sucessfully changed working directory to '%s' at FTP server '%s:%d'.",
                    FTP_IMAGE_DIR, FTP_HOST, FTP_PORT));
        }

        if (ftp.setFileType(FTP.BINARY_FILE_TYPE)) {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Sucessfully set file type to BINARY_FILE_TYPE at FTP server '%s:%d'.", FTP_HOST, FTP_PORT));
        } else {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("Failed to set file type to BINARY_FILE_TYPE at FTP server '%s:%d'.", FTP_HOST, FTP_PORT));
        }

        // Get a list of files to prevent saving already existing files
        fileList = Arrays.asList(ftp.listNames());

        // Log files found
        Logger.getAnonymousLogger().log(Level.INFO, "FTP: Existing files on FTP server...");
        for (String name : fileList) {
            Logger.getAnonymousLogger().log(Level.INFO, String.format("    %s", name));
        }
    }

    @PreDestroy
    private void disconnectFTP() {
        if (ftp != null) {
            Logger.getLogger(ImageDataManagerBean.class.getName()).log(Level.INFO, String.format("FTP: Disconnecting and logging off FTP server '%s:%d'.", FTP_HOST, FTP_PORT));

            try {
                ftp.logout();
            } catch (FTPConnectionClosedException e) {
                // Disconnected, do nothing.
            } catch (Exception ex) {
                Logger.getLogger(ImageDataManagerBean.class.getName()).log(Level.WARNING, "FTP: Failed to logout.", ex);
            }

            try {
                ftp.disconnect();
            } catch (FTPConnectionClosedException e) {
                // Disconnected, do nothing.
            } catch (Exception ex) {
                Logger.getLogger(ImageDataManagerBean.class.getName()).log(Level.WARNING, "FTP: Failed to disconnect.", ex);
            }
        }
    }

    public ImageCacheLocal getImageCache() {
        return imageCache;
    }

    public void setImageCache(ImageCacheLocal imageCache) {
        this.imageCache = imageCache;
    }
}
